import threading
import json
import tkinter as tk
from tkinter import filedialog, messagebox

import pandas as pd
from skhand import Camera, HandInput
from skhand.HandUtils.Drawing import draw_hand_data

from .tkGUI.tkToolGUI import tkToolGUI


class tkHandsDataTool(tkToolGUI):
    __slots__ = (
        "data_df",
        "data_file_path",
        "thread_dict",
        "close_camera",
        "need_to_writeback",
        "request_data",
        "writeback_event_id",
    )

    def __init__(self, root):
        super().__init__(root)
        # 创建一个DataFrame来保存当前数据文件里的数据
        self.data_df: pd.DataFrame | None = None
        # 记录当前数据保存文件路径
        self.data_file_path: str | None = None
        # 维护一个线程字典来,判断线程是否在运行
        self.thread_dict: dict[str, threading.Thread] = {}
        # 创建一个标志,用于控制线程的运行
        self.close_camera: threading.Event = threading.Event()
        # 创建线程事件,标志当前数据是否需要写回
        self.need_to_writeback: threading.Event = threading.Event()
        # 创建线程事件,用于请求返回手部数据至队列
        self.request_data: threading.Event = threading.Event()
        # 记录定时更新事件的id
        self.writeback_event_id: int | None = None
        # 为根窗口绑定键盘按键
        self.root.bind("<Key>", self.key_mapping_callback)
        # 绑定窗口关闭事件
        self.root.protocol("WM_DELETE_WINDOW", self.close_win_callback)

    """callback function"""

    def key_mapping_callback(self, event):
        """处理键盘按键映射事件"""
        if event.char == "a":
            self.save_btn_callback()
        elif event.char == "d":
            self.delete_btn_callback()
        elif event.char == "w":
            self.camera_btn_callback()
        elif event.char == "f":
            self.file_btn_callback()

    def close_win_callback(self):
        """关闭窗口回调函数"""
        # 先关闭窗口防止无响应中断而无法保存数据
        self.root.destroy()
        if self.data_df is not None:
            self.writeback(0)  # 立刻写回数据

    def camera_btn_callback(self):
        # 获取手部检测线程
        hi_loop_thread = self.thread_dict.get("hi_loop", None)
        # 手部检测正在运行
        if hi_loop_thread is not None and hi_loop_thread.is_alive():
            self.close_camera.set()  # 关闭摄像线程
        else:  # 手部检测线程未运行
            # 创建新的手部检测线程
            self.thread_dict["hi_loop"] = threading.Thread(
                name="hi_loop", target=self.hi_loop, daemon=True
            )
            self.thread_dict["hi_loop"].start()

    def save_btn_callback(self):
        # 检查是否已经选择了数据文件路径
        if self.data_file_path is None:
            self.path_label.config(text="请先选择要保存到的文件路径")
            messagebox.showwarning("保存失败", "请先选择要保存到的文件路径")
            return
        # 检查是否已经在运行手部检测线程
        hi_loop_thread = self.thread_dict.get("hi_loop", None)
        if hi_loop_thread is None or not hi_loop_thread.is_alive():
            messagebox.showwarning("保存失败", "请先打开摄像头,才能检测手部")
            return
        # 检查是否已经加载了数据
        if self.data_df is None:
            messagebox.showwarning("无法保存", "数据未加载完毕")
            return
        # 请求保存手部数据
        self.request_data.set()

    def file_btn_callback(self):
        # 在切换数据文件之前保存所有旧数据
        self.writeback(0)  # 立刻写回
        self.select_file()  # 选择文件

    def setting_save_btn_callback(self):
        if not messagebox.askyesno(
            "保存设置提示", "是否保存当前的文件路径和所有标签卡?"
        ):
            return
        # 获取所有设置的当前的值
        setting_dict = {
            "path": self.data_file_path,
            "default": self.cards_frame.get_default_input(),
            "cards": self.cards_frame.get_cards_dict(),
        }
        # 将Python对象写入JSON文件
        with open("tkHandDataToolSetting.json", "w", encoding="utf-8") as file:
            json.dump(setting_dict, file, ensure_ascii=False, indent=4)

    def setting_load_btn_callback(self):
        try:
            # 从JSON文件读取数据
            with open("tkHandDataToolSetting.json", "r", encoding="utf-8") as file:
                setting_dict = json.load(file)
            # 将之前的设置的值传回对应的位置
            # 设置数据保存文件路径
            data_file_path = setting_dict["path"]
            if data_file_path is not None:
                self.select_file(data_file_path)
            # 设置标签卡默认值的值
            default_value = setting_dict["default"]
            self.cards_frame.init_default_value(default_value)
            # 设置标签卡的值
            cards_value = setting_dict["cards"]
            self.cards_frame.init_cards(cards_value)
        except FileNotFoundError:
            messagebox.showwarning(
                "设置加载失败", "没有在当前目录下找到tkHandDataToolSetting.json文件"
            )
        except Exception as e:  # 未知错误
            messagebox.showwarning("设置加载错误", f"错误:{e}")

    def delete_btn_callback(self):
        # 没有加载数据,则索引恢复为默认值0
        if self.data_df is None:
            self.data_select.set(0)
            return
        # 有加载数据
        idx = int(self.data_select.get())
        allow_delete = messagebox.askyesno("删除提示", f"是否确定删除第{idx}条数据")
        if allow_delete:
            self.data_df.drop(index=idx, inplace=True)  # 删除数据
            self.data_select_callback()  # 调整索引并更新手部数据显示
            self.need_to_writeback.set()  # 标记数据需要写回
            self.writeback(10_000)  # 10秒数自动后写回

    def data_select_callback(self, event=None):
        # 没有加载数据,则索引恢复为默认值0
        if self.data_df is None:
            self.data_select.set(0)
            return
        # 有加载数据,则调整索引并显示手部数据
        row, _ = self.data_df.shape
        idx = int(self.data_select.get())
        if idx < 0 or idx >= row:  # 调整索引
            self.data_select.set(row - 1)
        self.show_data_img()  # 绘制数据
        return

    def hi_loop(self):
        """手部检测循环"""
        self.camera_btn.config(text="正在开启摄像头...", state=tk.DISABLED)
        # 初始化手部检测器和绘制器
        hand_input = HandInput(["hand0"])
        # 打开摄像头
        camera = Camera(0)
        self.camera_btn.config(text="关闭手部关键点检测(w)", state=tk.NORMAL)
        for img in camera.read():
            # 检查是否关闭摄像头
            if self.close_camera.is_set():
                break
            # 运行手部检测器
            if hand_input.run(img):
                # 响应数据请求
                if self.request_data.is_set():
                    hand_data = hand_input.data_unwrap("hand0")
                    self.save_data(hand_data.all2DataFrame)
                    self.request_data.clear()  # 返回数据之后就重置请求标志
                # 绘制并显示当前检测的画面
                hand_drawing = hand_input.drawing_unwrap("hand0")
                hand_drawing.draw_hand()
                hand_img = hand_drawing.draw_hand_only(padx=12, pady=12)
                if hand_img is not None:
                    self.detect_canvas.show_image(hand_img)
                self.camera_label.config(text=f"当前帧率为{camera.fps}")
                self.detect_label.config(text="检测到手部数据, 可以保存")
            else:
                # 响应数据请求
                if self.request_data.is_set():
                    messagebox.showwarning("保存失败", "没有检测到手部")
                    self.request_data.clear()  # 重置请求标志
                self.detect_canvas.show_nothing()
                self.camera_label.config(text="摄像头已开启")
                self.detect_label.config(text="未检测到手部数据")
        # 重置组件显示
        self.detect_canvas.show_nothing()
        self.camera_btn.config(text="开启手部关键点检测(w)")
        self.camera_label.config(text="未开启摄像头")
        self.detect_label.config(text="未开始检测手部数据")
        # 重置关闭摄像头线程标志
        self.close_camera.clear()

    def save_data(self, new_data_df: pd.DataFrame):
        """合并新的手部数据,标记需要写回标志"""
        # 检查是否已经加载了数据
        if self.data_df is None:
            messagebox.showwarning("无法保存", "数据未加载完毕")
            return
        # 将 保存数据按键 禁用
        self.save_btn.config(text="正在保存数据", state=tk.DISABLED)
        # 在手部数据DataFrame中添加一列记录当前时间
        new_data_df["RecordTime"] = pd.Timestamp.now()
        # 获取所有标签卡里的数据
        label_dict = self.cards_frame.get_all_label()
        # 将字典数据转换为DataFrame
        label_df = pd.DataFrame(label_dict, index=pd.Series([0]))
        # 将当前手部数据和标签数据合并成一行新数据
        updated_df = pd.concat([new_data_df, label_df], axis=1, ignore_index=True)
        # 重命名列名,因为上面只合并行的话会丢失列名
        updated_df.columns = list(new_data_df.columns) + list(label_df.columns)
        # 将新数据追加到现有数据
        self.data_df = pd.concat([self.data_df, updated_df], axis=0, ignore_index=True)
        # 处理完毕再重新启用按键
        self.save_btn.config(text="保存手部数据(a)", state=tk.NORMAL)
        # 显示最新的手部数据
        self.show_data_img(reset=True)
        # 追加成功则标记数据需要写回
        self.need_to_writeback.set()
        self.writeback(10_000)  # 10秒数后写回

    def writeback(self, ms_timer: int = 10_000):
        """定时写回数据,在指定时间内没有写回请求才创建写回线程进行写回"""
        # 如果已经存在写回事件则先取消之前的写回事件
        if self.writeback_event_id is not None:
            self.root.after_cancel(self.writeback_event_id)
        # 如果有写回线程在运行,则先让之前的写回线程运行完
        writeback_thread = self.thread_dict.get("writeback", None)
        if writeback_thread is not None and writeback_thread.is_alive():
            writeback_thread.join()
            # 完成上一次写回后可能会没有保存到最新数据,所以将数据设置为需要再次写回
            self.need_to_writeback.set()
        # 创建新的写回线程来写回新的数据
        self.thread_dict["writeback"] = threading.Thread(
            name="writeback", target=self.write_to_data_file
        )
        if ms_timer <= 0:  # 立刻写回,不创建新的线程,在当前线程直接写回
            self.write_to_data_file()
        else:  # 在指定时间后才写回
            self.writeback_event_id = self.root.after(
                ms_timer, self.thread_dict["writeback"].start
            )

    def write_to_data_file(self):
        """将数据data_df写回文件里"""
        # 重置写回事件id
        self.writeback_event_id = None
        # 检查是否有数据可以写回数据文件
        if self.data_file_path is None or self.data_df is None:
            return
        # 检查当前数据是否需要写回
        if not self.need_to_writeback.is_set():
            return
        try:  # 将所有数据写回数据文件里
            if self.data_file_path.lower().endswith((".xlsx", ".xls")):
                with pd.ExcelWriter(
                    self.data_file_path,
                    engine="openpyxl",
                    mode="a",
                    if_sheet_exists="replace",
                ) as writer:
                    self.data_df.to_excel(writer, sheet_name="Sheet1", index=False)
                self.need_to_writeback.clear()  # 写回完毕,重置写回标志
            elif self.data_file_path.lower().endswith((".csv")):
                self.data_df.to_csv(self.data_file_path, index=False)
                self.need_to_writeback.clear()  # 写回完毕,重置写回标志
            else:
                self.data_file_path = None  # 将文件路径重新设置回None
                self.path_label.config(text="该文件不是可用文件,请选择Excel或csv文件")
                messagebox.showwarning(
                    "数据保存失败",
                    f"数据文件的格式不可用\n路径:{self.data_file_path}",
                )
        except PermissionError:  # 没有权限写入文件
            messagebox.showwarning(
                "数据保存失败",
                f"没有权限打开数据文件,请确保你没有打开该文件\n路径:{self.data_file_path}",
            )
        except Exception as e:  # 未知错误
            self.data_file_path = None  # 将文件路径重新设置回None
            self.path_label.config(text="文件加载发生错误:\n" + str(e))
            messagebox.showwarning(
                "数据保存失败", f"错误:{e}\n路径:{self.data_file_path}"
            )

    def select_file(self, init_path: str | None = None):
        """设置文件路径并加载路径的数据"""
        # 弹出文件选择对话框
        self.data_file_path = (
            filedialog.askopenfilename(
                initialdir="./",
                title="请选择一个Excel或CSV文件",
                filetypes=[
                    ("Excel 文件", "*.xlsx *.xls"),
                    ("CSV 文件", "*.csv"),
                ],
            )
            # 用init_path来设置默认值,没有默认值则打开文件选择框
            if init_path is None
            else init_path
        )
        if self.data_file_path:  # 显示所选文件的路径
            self.path_label.config(text=f"{self.data_file_path}")
            # 选择完后就开一个线程用于加载数据,防止界面阻塞
            load_data_thread = self.thread_dict.get("load_data", None)
            # 如果有加载线程在运行,则先让之前的加载线程运行完
            if load_data_thread is not None and load_data_thread.is_alive():
                load_data_thread.join()
            self.thread_dict["load_data"] = threading.Thread(
                name="load_data", target=self.load_data_file, daemon=True
            )
            self.thread_dict["load_data"].start()
        else:  # 如果用户取消了选择
            self.data_file_path = None  # 将文件路径重新设置回None

    def load_data_file(self):
        """加载当前数据文件到内存中"""
        # 检查是否已经选择了数据文件路径
        if self.data_file_path is None:
            return
        # 将需要加载数据文件的按键都禁用
        self.file_btn.config(text="正在加载数据", state=tk.DISABLED)
        self.setting_load_btn.config(text="正在加载数据", state=tk.DISABLED)
        try:  # 加载数据文件中已有的数据到self.data_df里
            if self.data_file_path.lower().endswith((".xlsx", ".xls")):
                self.data_df = pd.read_excel(self.data_file_path, engine="openpyxl")
            elif self.data_file_path.lower().endswith((".csv")):
                self.data_df = pd.read_csv(self.data_file_path)
            else:  # 不是可用的数据文件格式
                self.data_file_path = None  # 将文件路径重新设置回None
                self.path_label.config(text="该文件不是可用文件,请选择Excel或csv文件")
        except FileNotFoundError:  # 没有该路径的文件
            self.data_file_path = None  # 将文件路径重新设置回None
            self.path_label.config(text="文件路径错误,请重新选择正确的文件路径")
        except PermissionError:  # 没有权限打开文件
            messagebox.showwarning(
                "数据文件打开失败",
                f"没有权限打开数据文件,请确保你没有打开该文件\n路径:{self.data_file_path}",
            )
        except Exception as e:  # 未知错误
            self.data_file_path = None  # 将文件路径重新设置回None
            self.path_label.config(text="文件加载发生错误:\n" + str(e))
        # 显示最新的手部数据
        self.show_data_img(reset=True)
        # 处理完毕再重新启用按键
        self.file_btn.config(text="设置数据文件路径(f)", state=tk.NORMAL)
        self.setting_load_btn.config(text="加载我的设置", state=tk.NORMAL)

    def show_data_img(self, *, reset=False):
        """在画布中显示当前索引的手部数据图像"""
        if self.data_df is None:  # 检查当前是否有数据可显示
            return
        if reset:  # 重置到最新的的手部数据
            row, _ = self.data_df.shape
            if row <= 0:  # 如果当前DataFrame里没有数据,则不显示
                return
            self.data_select.set(row - 1)
        # 获取当前数据索引并显示
        index = int(self.data_select.get())
        hand_data = self.data_df.iloc[index, :].to_numpy()
        data_img = draw_hand_data(hand_data)
        self.data_canvas.show_image(data_img)


if __name__ == "__main__":
    root = tk.Tk()
    app = tkHandsDataTool(root)
    app.run()
